Сервис по продаже автомобилей с пробегом разрабатывает приложение для привлечения новых клиентов. В нём можно быстро узнать рыночную стоимость своего автомобиля. В вашем распоряжении исторические данные: технические характеристики, комплектации и цены автомобилей. Необходимо построить модель для определения стоимости.
Заказчику важны:
# common
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
#correlation
import phik
# pipeline & preprocessing
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import (
OneHotEncoder,
LabelEncoder,
OrdinalEncoder,
StandardScaler,
MinMaxScaler,
RobustScaler
)
#model selection
from sklearn.model_selection import (
train_test_split,
RandomizedSearchCV,
GridSearchCV
)
#models
from sklearn.linear_model import (
LinearRegression,
Ridge,
Lasso
)
from sklearn.tree import DecisionTreeRegressor
from sklearn.dummy import DummyRegressor
from catboost import CatBoostRegressor
from xgboost import XGBRegressor
from lightgbm import LGBMRegressor
#metrics
from sklearn.metrics import (
root_mean_squared_error
)
from sklearn.impute import SimpleImputer
# constants
RANDOM_STATE = 42
df = pd.read_csv('/datasets/autos.csv')
df.head()
| DateCrawled | Price | VehicleType | RegistrationYear | Gearbox | Power | Model | Kilometer | RegistrationMonth | FuelType | Brand | Repaired | DateCreated | NumberOfPictures | PostalCode | LastSeen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2016-03-24 11:52:17 | 480 | NaN | 1993 | manual | 0 | golf | 150000 | 0 | petrol | volkswagen | NaN | 2016-03-24 00:00:00 | 0 | 70435 | 2016-04-07 03:16:57 |
| 1 | 2016-03-24 10:58:45 | 18300 | coupe | 2011 | manual | 190 | NaN | 125000 | 5 | gasoline | audi | yes | 2016-03-24 00:00:00 | 0 | 66954 | 2016-04-07 01:46:50 |
| 2 | 2016-03-14 12:52:21 | 9800 | suv | 2004 | auto | 163 | grand | 125000 | 8 | gasoline | jeep | NaN | 2016-03-14 00:00:00 | 0 | 90480 | 2016-04-05 12:47:46 |
| 3 | 2016-03-17 16:54:04 | 1500 | small | 2001 | manual | 75 | golf | 150000 | 6 | petrol | volkswagen | no | 2016-03-17 00:00:00 | 0 | 91074 | 2016-03-17 17:40:17 |
| 4 | 2016-03-31 17:25:20 | 3600 | small | 2008 | manual | 69 | fabia | 90000 | 7 | gasoline | skoda | no | 2016-03-31 00:00:00 | 0 | 60437 | 2016-04-06 10:17:21 |
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 354369 entries, 0 to 354368 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 DateCrawled 354369 non-null object 1 Price 354369 non-null int64 2 VehicleType 316879 non-null object 3 RegistrationYear 354369 non-null int64 4 Gearbox 334536 non-null object 5 Power 354369 non-null int64 6 Model 334664 non-null object 7 Kilometer 354369 non-null int64 8 RegistrationMonth 354369 non-null int64 9 FuelType 321474 non-null object 10 Brand 354369 non-null object 11 Repaired 283215 non-null object 12 DateCreated 354369 non-null object 13 NumberOfPictures 354369 non-null int64 14 PostalCode 354369 non-null int64 15 LastSeen 354369 non-null object dtypes: int64(7), object(9) memory usage: 43.3+ MB
Видны пропуски, несоответсвие типов данных (даты), плохой стиль названий столбцов - данным нужна предобработка
df.columns = df.columns.str.replace('(?<=[a-z])(?=[A-Z])', '_', regex=True).str.lower() #rename columns using regexp
df.head()
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2016-03-24 11:52:17 | 480 | NaN | 1993 | manual | 0 | golf | 150000 | 0 | petrol | volkswagen | NaN | 2016-03-24 00:00:00 | 0 | 70435 | 2016-04-07 03:16:57 |
| 1 | 2016-03-24 10:58:45 | 18300 | coupe | 2011 | manual | 190 | NaN | 125000 | 5 | gasoline | audi | yes | 2016-03-24 00:00:00 | 0 | 66954 | 2016-04-07 01:46:50 |
| 2 | 2016-03-14 12:52:21 | 9800 | suv | 2004 | auto | 163 | grand | 125000 | 8 | gasoline | jeep | NaN | 2016-03-14 00:00:00 | 0 | 90480 | 2016-04-05 12:47:46 |
| 3 | 2016-03-17 16:54:04 | 1500 | small | 2001 | manual | 75 | golf | 150000 | 6 | petrol | volkswagen | no | 2016-03-17 00:00:00 | 0 | 91074 | 2016-03-17 17:40:17 |
| 4 | 2016-03-31 17:25:20 | 3600 | small | 2008 | manual | 69 | fabia | 90000 | 7 | gasoline | skoda | no | 2016-03-31 00:00:00 | 0 | 60437 | 2016-04-06 10:17:21 |
Посмотрим, где есть пропуски
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 354369 entries, 0 to 354368 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date_crawled 354369 non-null object 1 price 354369 non-null int64 2 vehicle_type 316879 non-null object 3 registration_year 354369 non-null int64 4 gearbox 334536 non-null object 5 power 354369 non-null int64 6 model 334664 non-null object 7 kilometer 354369 non-null int64 8 registration_month 354369 non-null int64 9 fuel_type 321474 non-null object 10 brand 354369 non-null object 11 repaired 283215 non-null object 12 date_created 354369 non-null object 13 number_of_pictures 354369 non-null int64 14 postal_code 354369 non-null int64 15 last_seen 354369 non-null object dtypes: int64(7), object(9) memory usage: 43.3+ MB
Начнем с модели: ее пропуск связан с какой-то ошибкой, так как машин без модели не бывает, поэтому заполним константой
df['model'] = df['model'].fillna('unknown')
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 354369 entries, 0 to 354368 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date_crawled 354369 non-null object 1 price 354369 non-null int64 2 vehicle_type 316879 non-null object 3 registration_year 354369 non-null int64 4 gearbox 334536 non-null object 5 power 354369 non-null int64 6 model 354369 non-null object 7 kilometer 354369 non-null int64 8 registration_month 354369 non-null int64 9 fuel_type 321474 non-null object 10 brand 354369 non-null object 11 repaired 283215 non-null object 12 date_created 354369 non-null object 13 number_of_pictures 354369 non-null int64 14 postal_code 354369 non-null int64 15 last_seen 354369 non-null object dtypes: int64(7), object(9) memory usage: 43.3+ MB
Далее тип кузова: попробуем поискать в датасете такие же модели и заполнить пропуски в соответствии с заполненными типами кузова
np.sum(df.groupby('model')['vehicle_type'].last() != df.groupby('model')['vehicle_type'].first())
84
Интресно, что некоторые автомобили бывают в разных кузовах, что логично, поэтому попробуем заполнить модой. Все же лучше, чем 10% данных выбрасывать
Проверим пропуски:
df.groupby('model')['vehicle_type'].first().isna().sum()
0
df['vehicle_type'] = df['vehicle_type'].fillna(df.groupby('model')['vehicle_type'].transform(lambda x: x.mode()[0]))
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 354369 entries, 0 to 354368 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date_crawled 354369 non-null object 1 price 354369 non-null int64 2 vehicle_type 354369 non-null object 3 registration_year 354369 non-null int64 4 gearbox 334536 non-null object 5 power 354369 non-null int64 6 model 354369 non-null object 7 kilometer 354369 non-null int64 8 registration_month 354369 non-null int64 9 fuel_type 321474 non-null object 10 brand 354369 non-null object 11 repaired 283215 non-null object 12 date_created 354369 non-null object 13 number_of_pictures 354369 non-null int64 14 postal_code 354369 non-null int64 15 last_seen 354369 non-null object dtypes: int64(7), object(9) memory usage: 43.3+ MB
Далее тип коробки передач: для каждой модели он может быть любым так, что просто меняем на константу
df['gearbox'].unique()
array(['manual', 'auto', nan], dtype=object)
df['gearbox'] = df['gearbox'].fillna('unknown')
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 354369 entries, 0 to 354368 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date_crawled 354369 non-null object 1 price 354369 non-null int64 2 vehicle_type 354369 non-null object 3 registration_year 354369 non-null int64 4 gearbox 354369 non-null object 5 power 354369 non-null int64 6 model 354369 non-null object 7 kilometer 354369 non-null int64 8 registration_month 354369 non-null int64 9 fuel_type 321474 non-null object 10 brand 354369 non-null object 11 repaired 283215 non-null object 12 date_created 354369 non-null object 13 number_of_pictures 354369 non-null int64 14 postal_code 354369 non-null int64 15 last_seen 354369 non-null object dtypes: int64(7), object(9) memory usage: 43.3+ MB
посмотрим на тип топлива
df['fuel_type'].unique()
array(['petrol', 'gasoline', nan, 'lpg', 'other', 'hybrid', 'cng',
'electric'], dtype=object)
Пропуски тут можно заполнить по моделям, но тут тоже одна модель может иметь множество различных типов топлива, так что заполним, как other, который и так есть в этом столбце
df['fuel_type'] = df['fuel_type'].fillna('other')
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 354369 entries, 0 to 354368 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date_crawled 354369 non-null object 1 price 354369 non-null int64 2 vehicle_type 354369 non-null object 3 registration_year 354369 non-null int64 4 gearbox 354369 non-null object 5 power 354369 non-null int64 6 model 354369 non-null object 7 kilometer 354369 non-null int64 8 registration_month 354369 non-null int64 9 fuel_type 354369 non-null object 10 brand 354369 non-null object 11 repaired 283215 non-null object 12 date_created 354369 non-null object 13 number_of_pictures 354369 non-null int64 14 postal_code 354369 non-null int64 15 last_seen 354369 non-null object dtypes: int64(7), object(9) memory usage: 43.3+ MB
И, наконец, информация о том, была ли машина в ремонте или нет.
Тут пропуск несет в себе смысл - нет записи - не было ремонтов, поэтому заменим пропуски на no
df['repaired'] = df['repaired'].fillna('no')
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 354369 entries, 0 to 354368 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date_crawled 354369 non-null object 1 price 354369 non-null int64 2 vehicle_type 354369 non-null object 3 registration_year 354369 non-null int64 4 gearbox 354369 non-null object 5 power 354369 non-null int64 6 model 354369 non-null object 7 kilometer 354369 non-null int64 8 registration_month 354369 non-null int64 9 fuel_type 354369 non-null object 10 brand 354369 non-null object 11 repaired 354369 non-null object 12 date_created 354369 non-null object 13 number_of_pictures 354369 non-null int64 14 postal_code 354369 non-null int64 15 last_seen 354369 non-null object dtypes: int64(7), object(9) memory usage: 43.3+ MB
Заполнили пропуски везде, причем для каждого из признака с пропусками было около 10% пропусков, поэтому удалять не хотелось бы
df['date_crawled'] = pd.to_datetime(df['date_crawled'], format='%Y-%m-%d %H:%M:%S')
df['date_created'] = pd.to_datetime(df['date_created'], format='%Y-%m-%d %H:%M:%S')
df['last_seen'] = pd.to_datetime(df['last_seen'], format='%Y-%m-%d %H:%M:%S')
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 354369 entries, 0 to 354368 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date_crawled 354369 non-null datetime64[ns] 1 price 354369 non-null int64 2 vehicle_type 354369 non-null object 3 registration_year 354369 non-null int64 4 gearbox 354369 non-null object 5 power 354369 non-null int64 6 model 354369 non-null object 7 kilometer 354369 non-null int64 8 registration_month 354369 non-null int64 9 fuel_type 354369 non-null object 10 brand 354369 non-null object 11 repaired 354369 non-null object 12 date_created 354369 non-null datetime64[ns] 13 number_of_pictures 354369 non-null int64 14 postal_code 354369 non-null int64 15 last_seen 354369 non-null datetime64[ns] dtypes: datetime64[ns](3), int64(7), object(6) memory usage: 43.3+ MB
Посмотрим на явные дубликаты и удалим их при необходимости
df.duplicated().sum()
5
df = df.drop_duplicates().reset_index(drop=True)
df.duplicated().sum()
0
Теперь посмотрим на неявные дубликаты в значениях столбцов:
def find_implicit_duplicates(df):
for col in df.select_dtypes(exclude='number').columns:
print(col)
print(df.select_dtypes(exclude='number')[col].unique())
find_implicit_duplicates(df.select_dtypes(exclude='number').drop(columns=['date_crawled', 'date_created', 'last_seen']))
vehicle_type ['sedan' 'coupe' 'suv' 'small' 'convertible' 'bus' 'wagon' 'other'] gearbox ['manual' 'auto' 'unknown'] model ['golf' 'unknown' 'grand' 'fabia' '3er' '2_reihe' 'other' 'c_max' '3_reihe' 'passat' 'navara' 'ka' 'polo' 'twingo' 'a_klasse' 'scirocco' '5er' 'meriva' 'arosa' 'c4' 'civic' 'transporter' 'punto' 'e_klasse' 'clio' 'kadett' 'kangoo' 'corsa' 'one' 'fortwo' '1er' 'b_klasse' 'signum' 'astra' 'a8' 'jetta' 'fiesta' 'c_klasse' 'micra' 'vito' 'sprinter' '156' 'escort' 'forester' 'xc_reihe' 'scenic' 'a4' 'a1' 'insignia' 'combo' 'focus' 'tt' 'a6' 'jazz' 'omega' 'slk' '7er' '80' '147' '100' 'z_reihe' 'sportage' 'sorento' 'v40' 'ibiza' 'mustang' 'eos' 'touran' 'getz' 'a3' 'almera' 'megane' 'lupo' 'r19' 'zafira' 'caddy' 'mondeo' 'cordoba' 'colt' 'impreza' 'vectra' 'berlingo' 'tiguan' 'i_reihe' 'espace' 'sharan' '6_reihe' 'panda' 'up' 'seicento' 'ceed' '5_reihe' 'yeti' 'octavia' 'mii' 'rx_reihe' '6er' 'modus' 'fox' 'matiz' 'beetle' 'c1' 'rio' 'touareg' 'logan' 'spider' 'cuore' 's_max' 'a2' 'galaxy' 'c3' 'viano' 's_klasse' '1_reihe' 'avensis' 'roomster' 'sl' 'kaefer' 'santa' 'cooper' 'leon' '4_reihe' 'a5' '500' 'laguna' 'ptcruiser' 'clk' 'primera' 'x_reihe' 'exeo' '159' 'transit' 'juke' 'qashqai' 'carisma' 'accord' 'corolla' 'lanos' 'phaeton' 'verso' 'swift' 'rav' 'picanto' 'boxster' 'kalos' 'superb' 'stilo' 'alhambra' 'mx_reihe' 'roadster' 'ypsilon' 'cayenne' 'galant' 'justy' '90' 'sirion' 'crossfire' 'agila' 'duster' 'cr_reihe' 'v50' 'c_reihe' 'v_klasse' 'm_klasse' 'yaris' 'c5' 'aygo' 'cc' 'carnival' 'fusion' '911' 'bora' 'forfour' 'm_reihe' 'cl' 'tigra' '300c' 'spark' 'v70' 'kuga' 'x_type' 'ducato' 's_type' 'x_trail' 'toledo' 'altea' 'voyager' 'calibra' 'bravo' 'antara' 'tucson' 'citigo' 'jimny' 'wrangler' 'lybra' 'q7' 'lancer' 'captiva' 'c2' 'discovery' 'freelander' 'sandero' 'note' '900' 'cherokee' 'clubman' 'samara' 'defender' '601' 'cx_reihe' 'legacy' 'pajero' 'auris' 'niva' 's60' 'nubira' 'vivaro' 'g_klasse' 'lodgy' '850' 'range_rover' 'q3' 'serie_2' 'glk' 'charade' 'croma' 'outlander' 'doblo' 'musa' 'move' '9000' 'v60' '145' 'aveo' '200' 'b_max' 'range_rover_sport' 'terios' 'rangerover' 'q5' 'range_rover_evoque' 'materia' 'delta' 'gl' 'kalina' 'amarok' 'elefantino' 'i3' 'kappa' 'serie_3' 'serie_1'] fuel_type ['petrol' 'gasoline' 'other' 'lpg' 'hybrid' 'cng' 'electric'] brand ['volkswagen' 'audi' 'jeep' 'skoda' 'bmw' 'peugeot' 'ford' 'mazda' 'nissan' 'renault' 'mercedes_benz' 'opel' 'seat' 'citroen' 'honda' 'fiat' 'mini' 'smart' 'hyundai' 'sonstige_autos' 'alfa_romeo' 'subaru' 'volvo' 'mitsubishi' 'kia' 'suzuki' 'lancia' 'toyota' 'chevrolet' 'dacia' 'daihatsu' 'trabant' 'saab' 'chrysler' 'jaguar' 'daewoo' 'porsche' 'rover' 'land_rover' 'lada'] repaired ['no' 'yes']
Замечаем gasoline - petrol (означают бензин) в колонке и fuel_type и rangerover - range_rover (одна модель) в колонке . Избавимсяmodel
df = df.replace({'gasoline': 'petrol', 'rangerover': 'range_rover'})
find_implicit_duplicates(df.select_dtypes(exclude='number').drop(columns=['date_crawled', 'date_created', 'last_seen']))
vehicle_type ['sedan' 'coupe' 'suv' 'small' 'convertible' 'bus' 'wagon' 'other'] gearbox ['manual' 'auto' 'unknown'] model ['golf' 'unknown' 'grand' 'fabia' '3er' '2_reihe' 'other' 'c_max' '3_reihe' 'passat' 'navara' 'ka' 'polo' 'twingo' 'a_klasse' 'scirocco' '5er' 'meriva' 'arosa' 'c4' 'civic' 'transporter' 'punto' 'e_klasse' 'clio' 'kadett' 'kangoo' 'corsa' 'one' 'fortwo' '1er' 'b_klasse' 'signum' 'astra' 'a8' 'jetta' 'fiesta' 'c_klasse' 'micra' 'vito' 'sprinter' '156' 'escort' 'forester' 'xc_reihe' 'scenic' 'a4' 'a1' 'insignia' 'combo' 'focus' 'tt' 'a6' 'jazz' 'omega' 'slk' '7er' '80' '147' '100' 'z_reihe' 'sportage' 'sorento' 'v40' 'ibiza' 'mustang' 'eos' 'touran' 'getz' 'a3' 'almera' 'megane' 'lupo' 'r19' 'zafira' 'caddy' 'mondeo' 'cordoba' 'colt' 'impreza' 'vectra' 'berlingo' 'tiguan' 'i_reihe' 'espace' 'sharan' '6_reihe' 'panda' 'up' 'seicento' 'ceed' '5_reihe' 'yeti' 'octavia' 'mii' 'rx_reihe' '6er' 'modus' 'fox' 'matiz' 'beetle' 'c1' 'rio' 'touareg' 'logan' 'spider' 'cuore' 's_max' 'a2' 'galaxy' 'c3' 'viano' 's_klasse' '1_reihe' 'avensis' 'roomster' 'sl' 'kaefer' 'santa' 'cooper' 'leon' '4_reihe' 'a5' '500' 'laguna' 'ptcruiser' 'clk' 'primera' 'x_reihe' 'exeo' '159' 'transit' 'juke' 'qashqai' 'carisma' 'accord' 'corolla' 'lanos' 'phaeton' 'verso' 'swift' 'rav' 'picanto' 'boxster' 'kalos' 'superb' 'stilo' 'alhambra' 'mx_reihe' 'roadster' 'ypsilon' 'cayenne' 'galant' 'justy' '90' 'sirion' 'crossfire' 'agila' 'duster' 'cr_reihe' 'v50' 'c_reihe' 'v_klasse' 'm_klasse' 'yaris' 'c5' 'aygo' 'cc' 'carnival' 'fusion' '911' 'bora' 'forfour' 'm_reihe' 'cl' 'tigra' '300c' 'spark' 'v70' 'kuga' 'x_type' 'ducato' 's_type' 'x_trail' 'toledo' 'altea' 'voyager' 'calibra' 'bravo' 'antara' 'tucson' 'citigo' 'jimny' 'wrangler' 'lybra' 'q7' 'lancer' 'captiva' 'c2' 'discovery' 'freelander' 'sandero' 'note' '900' 'cherokee' 'clubman' 'samara' 'defender' '601' 'cx_reihe' 'legacy' 'pajero' 'auris' 'niva' 's60' 'nubira' 'vivaro' 'g_klasse' 'lodgy' '850' 'range_rover' 'q3' 'serie_2' 'glk' 'charade' 'croma' 'outlander' 'doblo' 'musa' 'move' '9000' 'v60' '145' 'aveo' '200' 'b_max' 'range_rover_sport' 'terios' 'q5' 'range_rover_evoque' 'materia' 'delta' 'gl' 'kalina' 'amarok' 'elefantino' 'i3' 'kappa' 'serie_3' 'serie_1'] fuel_type ['petrol' 'other' 'lpg' 'hybrid' 'cng' 'electric'] brand ['volkswagen' 'audi' 'jeep' 'skoda' 'bmw' 'peugeot' 'ford' 'mazda' 'nissan' 'renault' 'mercedes_benz' 'opel' 'seat' 'citroen' 'honda' 'fiat' 'mini' 'smart' 'hyundai' 'sonstige_autos' 'alfa_romeo' 'subaru' 'volvo' 'mitsubishi' 'kia' 'suzuki' 'lancia' 'toyota' 'chevrolet' 'dacia' 'daihatsu' 'trabant' 'saab' 'chrysler' 'jaguar' 'daewoo' 'porsche' 'rover' 'land_rover' 'lada'] repaired ['no' 'yes']
Теперь посмотрим на неявные дубликаты с другой стороны - попробуем исключить информацию о человеке, и о базе данных а оставить только о машине. Посмотрим:
df.head()
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2016-03-24 11:52:17 | 480 | sedan | 1993 | manual | 0 | golf | 150000 | 0 | petrol | volkswagen | no | 2016-03-24 | 0 | 70435 | 2016-04-07 03:16:57 |
| 1 | 2016-03-24 10:58:45 | 18300 | coupe | 2011 | manual | 190 | unknown | 125000 | 5 | petrol | audi | yes | 2016-03-24 | 0 | 66954 | 2016-04-07 01:46:50 |
| 2 | 2016-03-14 12:52:21 | 9800 | suv | 2004 | auto | 163 | grand | 125000 | 8 | petrol | jeep | no | 2016-03-14 | 0 | 90480 | 2016-04-05 12:47:46 |
| 3 | 2016-03-17 16:54:04 | 1500 | small | 2001 | manual | 75 | golf | 150000 | 6 | petrol | volkswagen | no | 2016-03-17 | 0 | 91074 | 2016-03-17 17:40:17 |
| 4 | 2016-03-31 17:25:20 | 3600 | small | 2008 | manual | 69 | fabia | 90000 | 7 | petrol | skoda | no | 2016-03-31 | 0 | 60437 | 2016-04-06 10:17:21 |
Оставим дату в публикации фичах, потому что от нее может зависеть цена (инфляция и т.д).
df.drop(columns=['date_crawled', 'number_of_pictures', 'postal_code', 'last_seen', 'date_created']).duplicated().sum()
31181
df.drop(columns=['date_crawled', 'number_of_pictures', 'postal_code', 'last_seen', 'date_created', 'price']).duplicated().sum()
111250
Если дропать таргет в том числе, то у нас 111000+ повторов. То есть в датасете есть машины с абсолютно с одинаковыми хакартеристиками, но с разной ценой. Это даже обосновано, потому что машины, которые продаются +- зачастую одинаковые (даже абсолютно), но цену каждый ставит свою (кому-то надо срочно продать, кто-то перекуп), поэтому одинаковые авто с разной ценой оставим в датасете
df = df.drop_duplicates(subset=
df.drop(columns=['date_crawled', 'number_of_pictures', 'postal_code', 'last_seen', 'date_created']).columns).\
reset_index(drop=True)
df.drop(columns=['date_crawled', 'number_of_pictures', 'postal_code', 'last_seen', 'date_created']).duplicated().sum()
0
def print_hist_box(df, bins=50):
for col in df.columns:
print(df[col].describe())
f, (ax_box, ax_hist) = plt.subplots(2, sharex=True,
gridspec_kw={"height_ratios": (.15, .85)})
sns.boxplot(x=df[col], ax=ax_box)
ax_box.set_title('Боксплот и распределение призанака ' + col)
ax_box.set_xlabel('')
plt.ylabel('Количество элементов признака ' + col)
plt.xlabel('Значения элементов признака ' + col)
sns.histplot(x=df[col], bins=bins, kde=True, ax=ax_hist)
ax_box.set(yticks=[])
sns.despine(ax=ax_hist)
sns.despine(ax=ax_box, left=True)
plt.show()
def fmt(x): #func to determine labels on pie plot
return '{:.1f}%\n{:.2f}'.format(x, total * x / 100)
def print_pie_bar(df):
for col in df.columns:
print(df[col].describe())
plt.Figure(figsize=(30, 20))
sns.set_theme(rc={'figure.figsize':(11.7,8.27)})
ax = sns.countplot(x=df[col], order=df[col].value_counts().index)
for rect in ax.patches:
ax.text(rect.get_x() + rect.get_width() / 2, rect.get_height() + 0.75,rect.get_height(), horizontalalignment='center', fontsize=11)
plt.ylabel('Количество')
plt.xlabel('Значение')
plt.title('Столбчатая диаграмма признака ' + col)
ax.tick_params(axis='x', labelrotation=90)
plt.show()
global total
total = df[col].count()
df[col].value_counts().plot(kind='pie', autopct=fmt)
plt.title('Круговая диаграмма признака ' + col)
plt.ylabel('')
plt.show()
df.head()
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2016-03-24 11:52:17 | 480 | sedan | 1993 | manual | 0 | golf | 150000 | 0 | petrol | volkswagen | no | 2016-03-24 | 0 | 70435 | 2016-04-07 03:16:57 |
| 1 | 2016-03-24 10:58:45 | 18300 | coupe | 2011 | manual | 190 | unknown | 125000 | 5 | petrol | audi | yes | 2016-03-24 | 0 | 66954 | 2016-04-07 01:46:50 |
| 2 | 2016-03-14 12:52:21 | 9800 | suv | 2004 | auto | 163 | grand | 125000 | 8 | petrol | jeep | no | 2016-03-14 | 0 | 90480 | 2016-04-05 12:47:46 |
| 3 | 2016-03-17 16:54:04 | 1500 | small | 2001 | manual | 75 | golf | 150000 | 6 | petrol | volkswagen | no | 2016-03-17 | 0 | 91074 | 2016-03-17 17:40:17 |
| 4 | 2016-03-31 17:25:20 | 3600 | small | 2008 | manual | 69 | fabia | 90000 | 7 | petrol | skoda | no | 2016-03-31 | 0 | 60437 | 2016-04-06 10:17:21 |
#year and month are categorial, postal code is non informative
print_hist_box(df.select_dtypes(include='number').drop(columns=['registration_year', 'registration_month', 'postal_code']))
count 323183.000000 mean 4409.367857 std 4523.666589 min 0.000000 25% 1000.000000 50% 2700.000000 75% 6390.000000 max 20000.000000 Name: price, dtype: float64
count 323183.000000 mean 110.290458 std 196.895182 min 0.000000 25% 69.000000 50% 105.000000 75% 141.000000 max 20000.000000 Name: power, dtype: float64
count 323183.000000 mean 128056.905840 std 38017.336057 min 5000.000000 25% 125000.000000 50% 150000.000000 75% 150000.000000 max 150000.000000 Name: kilometer, dtype: float64
count 323183.0 mean 0.0 std 0.0 min 0.0 25% 0.0 50% 0.0 75% 0.0 max 0.0 Name: number_of_pictures, dtype: float64
price - таргет имеет ненормальное распределение, попробуем прологарифмировать его в следющем пункте, чтоб получить нормальное, потому что кто бы что не говорил, а в задачах регрессии лучше, чтобы таргет имел нормальное распределениеdf[df['price'] < 500]
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2016-03-24 11:52:17 | 480 | sedan | 1993 | manual | 0 | golf | 150000 | 0 | petrol | volkswagen | no | 2016-03-24 | 0 | 70435 | 2016-04-07 03:16:57 |
| 7 | 2016-03-21 18:54:38 | 0 | sedan | 1980 | manual | 50 | other | 40000 | 7 | petrol | volkswagen | no | 2016-03-21 | 0 | 19348 | 2016-03-25 16:47:58 |
| 15 | 2016-03-11 21:39:15 | 450 | small | 1910 | unknown | 0 | ka | 5000 | 0 | petrol | ford | no | 2016-03-11 | 0 | 24148 | 2016-03-19 08:46:47 |
| 16 | 2016-04-01 12:46:46 | 300 | small | 2016 | unknown | 60 | polo | 150000 | 0 | petrol | volkswagen | no | 2016-04-01 | 0 | 38871 | 2016-04-01 12:46:46 |
| 23 | 2016-03-12 19:43:07 | 450 | small | 1997 | manual | 50 | arosa | 150000 | 5 | petrol | seat | no | 2016-03-12 | 0 | 9526 | 2016-03-21 01:46:11 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 323142 | 2016-03-15 19:57:11 | 400 | wagon | 1991 | manual | 0 | legacy | 150000 | 0 | petrol | subaru | no | 2016-03-15 | 0 | 24558 | 2016-03-19 15:49:00 |
| 323150 | 2016-03-30 20:55:30 | 350 | small | 1996 | unknown | 65 | punto | 150000 | 0 | other | fiat | no | 2016-03-30 | 0 | 25436 | 2016-04-07 13:50:41 |
| 323155 | 2016-04-04 14:41:28 | 390 | small | 1997 | auto | 0 | corsa | 100000 | 6 | petrol | opel | yes | 2016-04-04 | 0 | 17509 | 2016-04-06 15:46:11 |
| 323156 | 2016-03-31 19:52:33 | 180 | sedan | 1995 | unknown | 0 | unknown | 125000 | 3 | petrol | opel | no | 2016-03-31 | 0 | 41470 | 2016-04-06 14:18:04 |
| 323178 | 2016-03-21 09:50:58 | 0 | sedan | 2005 | manual | 0 | colt | 150000 | 7 | petrol | mitsubishi | yes | 2016-03-21 | 0 | 2694 | 2016-03-21 10:42:49 |
33208 rows × 16 columns
Тут придется удалить 34000+ строк :( потому что как ни крути, а это либо объявления с ошибкой, либо как-то странно проданные автомобили, но восстановить цену точно не получится. Даже если это и реальные цены по каким то причинам, то эти причины в датасете, как правило, не указаны, так что эти машины будут вносить сильный шум. А так как это таргет, то и экспериментировать со вставкой среднего вместо 0 и т.д. не стоит. Теряем примерно 9.5 % данных. То же самое можно сказть и про аномально высокие цены машины, но, просмотрев их, я увидел там бмв, форд мустанг и так далее. Иногда проскакивают конечно и выбросы, но все таки все машины с высокой ценой удалять думаю не стоит, несмотря на некоторые выбросы, хотя на 2016 год 20 000 евро это 1,5 млн. руб, так что их не трогаем
df = df.drop(df[df['price'] < 500].index)
df.info()
<class 'pandas.core.frame.DataFrame'> Index: 289975 entries, 1 to 323182 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date_crawled 289975 non-null datetime64[ns] 1 price 289975 non-null int64 2 vehicle_type 289975 non-null object 3 registration_year 289975 non-null int64 4 gearbox 289975 non-null object 5 power 289975 non-null int64 6 model 289975 non-null object 7 kilometer 289975 non-null int64 8 registration_month 289975 non-null int64 9 fuel_type 289975 non-null object 10 brand 289975 non-null object 11 repaired 289975 non-null object 12 date_created 289975 non-null datetime64[ns] 13 number_of_pictures 289975 non-null int64 14 postal_code 289975 non-null int64 15 last_seen 289975 non-null datetime64[ns] dtypes: datetime64[ns](3), int64(7), object(6) memory usage: 37.6+ MB
print_hist_box(df['price'].to_frame())
count 289975.000000 mean 4891.624237 std 4532.157376 min 500.000000 25% 1490.000000 50% 3200.000000 75% 6950.000000 max 20000.000000 Name: price, dtype: float64
power - мощность в л.с. Видны явные выбросы. Посмотрим на нихdf[(df['power'] > 500) & (df['power'] < 1000)].head(10)
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 4052 | 2016-04-03 20:31:00 | 3100 | sedan | 2005 | manual | 953 | colt | 150000 | 4 | petrol | mitsubishi | no | 2016-04-03 | 0 | 60326 | 2016-04-07 14:56:46 |
| 6278 | 2016-03-31 23:50:47 | 599 | small | 2002 | manual | 603 | matiz | 5000 | 11 | petrol | chevrolet | yes | 2016-03-31 | 0 | 44379 | 2016-04-01 03:41:52 |
| 6482 | 2016-03-14 15:54:34 | 3000 | small | 2009 | manual | 771 | punto | 125000 | 0 | petrol | fiat | no | 2016-03-14 | 0 | 40721 | 2016-03-14 15:54:34 |
| 6615 | 2016-03-31 19:48:22 | 600 | small | 1996 | manual | 603 | corsa | 150000 | 8 | petrol | opel | yes | 2016-03-31 | 0 | 70327 | 2016-04-06 14:17:51 |
| 8026 | 2016-03-15 13:49:25 | 500 | small | 2002 | manual | 620 | ypsilon | 150000 | 12 | petrol | lancia | no | 2016-03-15 | 0 | 55566 | 2016-04-06 03:15:27 |
| 12668 | 2016-03-09 18:43:44 | 3500 | convertible | 2003 | manual | 952 | ka | 70000 | 5 | petrol | ford | no | 2016-03-09 | 0 | 26903 | 2016-03-12 04:16:07 |
| 13294 | 2016-03-31 12:38:14 | 18500 | small | 2002 | auto | 600 | s_type | 150000 | 12 | other | jaguar | no | 2016-03-31 | 0 | 13595 | 2016-04-06 04:46:34 |
| 14460 | 2016-03-12 09:55:06 | 599 | small | 2018 | manual | 599 | twingo | 150000 | 0 | other | renault | no | 2016-03-12 | 0 | 47138 | 2016-04-06 01:45:44 |
| 17374 | 2016-03-23 18:59:00 | 15500 | coupe | 2009 | auto | 507 | m_reihe | 150000 | 1 | petrol | bmw | yes | 2016-03-23 | 0 | 74254 | 2016-03-31 07:44:43 |
| 19957 | 2016-03-21 10:37:15 | 18500 | suv | 2006 | auto | 521 | cayenne | 150000 | 5 | petrol | porsche | no | 2016-03-21 | 0 | 24939 | 2016-04-06 03:16:48 |
df[df['power'] > 10000].head(10)
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 24963 | 2016-03-28 19:57:39 | 10900 | bus | 2009 | manual | 10520 | caddy | 150000 | 6 | petrol | volkswagen | no | 2016-03-28 | 0 | 36272 | 2016-04-07 02:47:02 |
| 44290 | 2016-04-07 07:36:38 | 15800 | convertible | 2014 | auto | 10218 | fortwo | 30000 | 3 | petrol | smart | no | 2016-04-06 | 0 | 81373 | 2016-04-07 07:36:38 |
| 55808 | 2016-04-01 21:55:58 | 1995 | wagon | 2002 | manual | 11530 | focus | 150000 | 1 | petrol | ford | no | 2016-04-01 | 0 | 35759 | 2016-04-05 14:44:36 |
| 61606 | 2016-03-27 18:47:59 | 2200 | small | 1999 | manual | 12012 | polo | 150000 | 3 | petrol | volkswagen | no | 2016-03-27 | 0 | 9526 | 2016-04-01 19:44:55 |
| 62572 | 2016-03-28 11:49:56 | 3250 | sedan | 2001 | auto | 17932 | omega | 150000 | 6 | petrol | opel | no | 2016-03-28 | 0 | 86641 | 2016-04-06 14:17:21 |
| 75819 | 2016-03-25 13:36:57 | 850 | sedan | 2000 | manual | 12510 | astra | 30000 | 10 | other | opel | no | 2016-03-25 | 0 | 23611 | 2016-03-31 20:45:39 |
| 82494 | 2016-03-28 10:49:33 | 1999 | sedan | 1991 | auto | 10912 | unknown | 150000 | 0 | petrol | mercedes_benz | no | 2016-03-28 | 0 | 88069 | 2016-03-30 01:15:32 |
| 93449 | 2016-03-16 13:46:18 | 1380 | convertible | 2001 | auto | 10710 | megane | 150000 | 10 | other | renault | no | 2016-03-16 | 0 | 71282 | 2016-03-19 10:45:58 |
| 93653 | 2016-03-12 10:36:18 | 4700 | bus | 1997 | manual | 10522 | transporter | 150000 | 0 | petrol | volkswagen | no | 2016-03-12 | 0 | 87437 | 2016-03-12 10:36:18 |
| 95559 | 2016-03-31 18:54:51 | 5500 | wagon | 2010 | manual | 11509 | ceed | 150000 | 9 | other | kia | no | 2016-03-31 | 0 | 15907 | 2016-04-06 13:15:34 |
можем поправить выбросы сократив порядок больших, но некоторые модели и правда не являются выбросами - например порш кайен с 500+ л.с. вполне нормальная практика. Поэтому чтоб потерять минимум информации сначала заменим на таких выбивающихся моделях этот показатель на медианный по модели (среднее будет смещено как раз из-за выбросов)
df.loc[df['power'] < 10, 'power'] = df.groupby('model')['power'].transform('median')
df[df['power'] < 10]
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 219458 | 2016-03-30 11:39:08 | 3800 | wagon | 1978 | manual | 0.0 | serie_1 | 30000 | 0 | petrol | land_rover | no | 2016-03-30 | 0 | 49824 | 2016-03-30 11:39:08 |
df.loc[df['power'] > 500, 'power'] = df.groupby('model')['power'].transform('median')
df[df['power'] > 500]
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen |
|---|
print_hist_box(df['power'].to_frame())
count 289975.000000 mean 121.013531 std 52.361346 min 0.000000 25% 82.000000 50% 111.000000 75% 150.000000 max 500.000000 Name: power, dtype: float64
У нас есть одно редкое авто, так еще и с ошибкой. Удаляем его
df.loc[df['power'] < 10].head(10)
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 219458 | 2016-03-30 11:39:08 | 3800 | wagon | 1978 | manual | 0.0 | serie_1 | 30000 | 0 | petrol | land_rover | no | 2016-03-30 | 0 | 49824 | 2016-03-30 11:39:08 |
df = df.drop(df[df['power'] < 10].index)
df[df['power'] < 10]
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen |
|---|
print_hist_box(df['power'].to_frame())
count 289974.000000 mean 121.013949 std 52.360954 min 10.000000 25% 82.000000 50% 111.000000 75% 150.000000 max 500.000000 Name: power, dtype: float64
Оставшиеся выбросы не являются аномалиями, так что оставить можно
kilometer - пробег - распределение не являтся нормальным, причем больше, чем у половины машин пробег строго равен 150 000. В целом действительности может соответствовать, но значение, по-видимому, округленное. Выбросы аномалиями не являются, так что оставляемИ ни для одной машины не указыно ни одной фотографии, так что этот признак можно убать, полную работу с фичами произведем на 3 шаге
df.head()
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 2016-03-24 10:58:45 | 18300 | coupe | 2011 | manual | 190.0 | unknown | 125000 | 5 | petrol | audi | yes | 2016-03-24 | 0 | 66954 | 2016-04-07 01:46:50 |
| 2 | 2016-03-14 12:52:21 | 9800 | suv | 2004 | auto | 163.0 | grand | 125000 | 8 | petrol | jeep | no | 2016-03-14 | 0 | 90480 | 2016-04-05 12:47:46 |
| 3 | 2016-03-17 16:54:04 | 1500 | small | 2001 | manual | 75.0 | golf | 150000 | 6 | petrol | volkswagen | no | 2016-03-17 | 0 | 91074 | 2016-03-17 17:40:17 |
| 4 | 2016-03-31 17:25:20 | 3600 | small | 2008 | manual | 69.0 | fabia | 90000 | 7 | petrol | skoda | no | 2016-03-31 | 0 | 60437 | 2016-04-06 10:17:21 |
| 5 | 2016-04-04 17:36:23 | 650 | sedan | 1995 | manual | 102.0 | 3er | 150000 | 10 | petrol | bmw | yes | 2016-04-04 | 0 | 33775 | 2016-04-06 19:17:07 |
Посмотрим на даты публикации объявлений. Это нам нужно для последющего анализа
sorted(df['date_created'].unique())
[Timestamp('2014-03-10 00:00:00'),
Timestamp('2015-03-20 00:00:00'),
Timestamp('2015-06-18 00:00:00'),
Timestamp('2015-08-07 00:00:00'),
Timestamp('2015-08-10 00:00:00'),
Timestamp('2015-09-04 00:00:00'),
Timestamp('2015-09-09 00:00:00'),
Timestamp('2015-11-02 00:00:00'),
Timestamp('2015-11-08 00:00:00'),
Timestamp('2015-11-10 00:00:00'),
Timestamp('2015-11-12 00:00:00'),
Timestamp('2015-11-17 00:00:00'),
Timestamp('2015-11-23 00:00:00'),
Timestamp('2015-11-24 00:00:00'),
Timestamp('2015-12-05 00:00:00'),
Timestamp('2015-12-06 00:00:00'),
Timestamp('2015-12-17 00:00:00'),
Timestamp('2015-12-27 00:00:00'),
Timestamp('2015-12-30 00:00:00'),
Timestamp('2016-01-02 00:00:00'),
Timestamp('2016-01-03 00:00:00'),
Timestamp('2016-01-06 00:00:00'),
Timestamp('2016-01-07 00:00:00'),
Timestamp('2016-01-08 00:00:00'),
Timestamp('2016-01-10 00:00:00'),
Timestamp('2016-01-13 00:00:00'),
Timestamp('2016-01-15 00:00:00'),
Timestamp('2016-01-16 00:00:00'),
Timestamp('2016-01-17 00:00:00'),
Timestamp('2016-01-18 00:00:00'),
Timestamp('2016-01-19 00:00:00'),
Timestamp('2016-01-22 00:00:00'),
Timestamp('2016-01-23 00:00:00'),
Timestamp('2016-01-24 00:00:00'),
Timestamp('2016-01-25 00:00:00'),
Timestamp('2016-01-26 00:00:00'),
Timestamp('2016-01-27 00:00:00'),
Timestamp('2016-01-28 00:00:00'),
Timestamp('2016-01-29 00:00:00'),
Timestamp('2016-01-30 00:00:00'),
Timestamp('2016-01-31 00:00:00'),
Timestamp('2016-02-01 00:00:00'),
Timestamp('2016-02-02 00:00:00'),
Timestamp('2016-02-03 00:00:00'),
Timestamp('2016-02-05 00:00:00'),
Timestamp('2016-02-06 00:00:00'),
Timestamp('2016-02-07 00:00:00'),
Timestamp('2016-02-08 00:00:00'),
Timestamp('2016-02-09 00:00:00'),
Timestamp('2016-02-10 00:00:00'),
Timestamp('2016-02-11 00:00:00'),
Timestamp('2016-02-12 00:00:00'),
Timestamp('2016-02-13 00:00:00'),
Timestamp('2016-02-14 00:00:00'),
Timestamp('2016-02-15 00:00:00'),
Timestamp('2016-02-16 00:00:00'),
Timestamp('2016-02-17 00:00:00'),
Timestamp('2016-02-18 00:00:00'),
Timestamp('2016-02-19 00:00:00'),
Timestamp('2016-02-20 00:00:00'),
Timestamp('2016-02-21 00:00:00'),
Timestamp('2016-02-22 00:00:00'),
Timestamp('2016-02-23 00:00:00'),
Timestamp('2016-02-24 00:00:00'),
Timestamp('2016-02-25 00:00:00'),
Timestamp('2016-02-26 00:00:00'),
Timestamp('2016-02-27 00:00:00'),
Timestamp('2016-02-28 00:00:00'),
Timestamp('2016-02-29 00:00:00'),
Timestamp('2016-03-01 00:00:00'),
Timestamp('2016-03-02 00:00:00'),
Timestamp('2016-03-03 00:00:00'),
Timestamp('2016-03-04 00:00:00'),
Timestamp('2016-03-05 00:00:00'),
Timestamp('2016-03-06 00:00:00'),
Timestamp('2016-03-07 00:00:00'),
Timestamp('2016-03-08 00:00:00'),
Timestamp('2016-03-09 00:00:00'),
Timestamp('2016-03-10 00:00:00'),
Timestamp('2016-03-11 00:00:00'),
Timestamp('2016-03-12 00:00:00'),
Timestamp('2016-03-13 00:00:00'),
Timestamp('2016-03-14 00:00:00'),
Timestamp('2016-03-15 00:00:00'),
Timestamp('2016-03-16 00:00:00'),
Timestamp('2016-03-17 00:00:00'),
Timestamp('2016-03-18 00:00:00'),
Timestamp('2016-03-19 00:00:00'),
Timestamp('2016-03-20 00:00:00'),
Timestamp('2016-03-21 00:00:00'),
Timestamp('2016-03-22 00:00:00'),
Timestamp('2016-03-23 00:00:00'),
Timestamp('2016-03-24 00:00:00'),
Timestamp('2016-03-25 00:00:00'),
Timestamp('2016-03-26 00:00:00'),
Timestamp('2016-03-27 00:00:00'),
Timestamp('2016-03-28 00:00:00'),
Timestamp('2016-03-29 00:00:00'),
Timestamp('2016-03-30 00:00:00'),
Timestamp('2016-03-31 00:00:00'),
Timestamp('2016-04-01 00:00:00'),
Timestamp('2016-04-02 00:00:00'),
Timestamp('2016-04-03 00:00:00'),
Timestamp('2016-04-04 00:00:00'),
Timestamp('2016-04-05 00:00:00'),
Timestamp('2016-04-06 00:00:00'),
Timestamp('2016-04-07 00:00:00')]
видим объявления с марта 2014 по апрель 2016
print_pie_bar(df[['vehicle_type', 'registration_year', 'gearbox', 'model', 'registration_month', 'fuel_type', 'brand', 'repaired']])
count 289974 unique 8 top sedan freq 91340 Name: vehicle_type, dtype: object
count 289974.000000 mean 2004.184858 std 67.681186 min 1000.000000 25% 1999.000000 50% 2004.000000 75% 2008.000000 max 9999.000000 Name: registration_year, dtype: float64
count 289974 unique 3 top manual freq 220183 Name: gearbox, dtype: object
count 289974 unique 249 top golf freq 23364 Name: model, dtype: object
count 289974.000000 mean 5.880065 std 3.648074 min 0.000000 25% 3.000000 50% 6.000000 75% 9.000000 max 12.000000 Name: registration_month, dtype: float64
count 289974 unique 6 top petrol freq 261973 Name: fuel_type, dtype: object
count 289974 unique 40 top volkswagen freq 61814 Name: brand, dtype: object
count 289974 unique 2 top no freq 264826 Name: repaired, dtype: object
Интересная картина в некоторых местах. Пойдем разбираться по одному:
Далее год регистрации - год регистрации (и месяц) не может быть больше года и месяца даты публикации, потому что мы же не можем продавать машину, которую зарегистрировали в будующем - поэтому сменим для таких автомобилей год и месяц на дату публикации (может какие-то перекупы, сегодня купили, помыли, и тут же перепродают)=)
df[df['registration_year'] > 2016]
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 22 | 2016-03-23 14:52:51 | 2900 | bus | 2018 | manual | 90.0 | meriva | 150000 | 5 | petrol | opel | no | 2016-03-23 | 0 | 49716 | 2016-03-31 01:16:33 |
| 26 | 2016-03-10 19:38:18 | 5555 | sedan | 2017 | manual | 125.0 | c4 | 125000 | 4 | other | citroen | no | 2016-03-10 | 0 | 31139 | 2016-03-16 09:16:46 |
| 48 | 2016-03-25 14:40:12 | 7750 | sedan | 2017 | manual | 80.0 | golf | 100000 | 1 | petrol | volkswagen | no | 2016-03-25 | 0 | 48499 | 2016-03-31 21:47:44 |
| 51 | 2016-03-07 18:57:08 | 2000 | small | 2017 | manual | 90.0 | punto | 150000 | 11 | petrol | fiat | yes | 2016-03-07 | 0 | 66115 | 2016-03-07 18:57:08 |
| 57 | 2016-03-10 20:53:19 | 2399 | sedan | 2018 | manual | 64.0 | other | 125000 | 3 | other | seat | no | 2016-03-10 | 0 | 33397 | 2016-03-25 10:17:37 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 322975 | 2016-03-11 15:49:51 | 3600 | bus | 2017 | manual | 86.0 | transit | 150000 | 5 | petrol | ford | no | 2016-03-11 | 0 | 32339 | 2016-03-12 05:45:02 |
| 322998 | 2016-03-29 16:47:29 | 1000 | wagon | 2017 | manual | 101.0 | a4 | 150000 | 9 | other | audi | no | 2016-03-29 | 0 | 38315 | 2016-04-06 02:44:27 |
| 323046 | 2016-03-17 00:56:26 | 2140 | small | 2018 | manual | 80.0 | fiesta | 150000 | 6 | other | ford | no | 2016-03-17 | 0 | 44866 | 2016-03-29 15:45:04 |
| 323090 | 2016-03-25 09:37:59 | 1250 | small | 2018 | unknown | 60.0 | corsa | 150000 | 0 | petrol | opel | no | 2016-03-25 | 0 | 45527 | 2016-04-06 07:46:13 |
| 323119 | 2016-03-05 14:55:29 | 5000 | sedan | 2017 | manual | 120.0 | other | 150000 | 7 | other | citroen | yes | 2016-03-05 | 0 | 15518 | 2016-04-05 11:48:09 |
12360 rows × 16 columns
df['registration_year'] = df.apply(lambda x: x['date_created'].year
if x['registration_year'] > x['date_created'].year
else x['registration_year'], axis=1)
df[df['registration_year'] > df['date_created'].dt.year]
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen |
|---|
df[df['registration_year'] < 1960]
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1925 | 2016-03-25 15:58:21 | 7000 | suv | 1945 | manual | 48.0 | other | 150000 | 2 | petrol | volkswagen | no | 2016-03-25 | 0 | 58135 | 2016-03-25 15:58:21 |
| 2270 | 2016-03-15 21:44:32 | 1800 | convertible | 1925 | unknown | 90.0 | unknown | 5000 | 1 | other | sonstige_autos | no | 2016-03-15 | 0 | 79288 | 2016-04-07 05:15:34 |
| 3329 | 2016-03-15 21:36:20 | 10500 | sedan | 1955 | manual | 30.0 | other | 60000 | 0 | petrol | ford | no | 2016-03-15 | 0 | 53498 | 2016-04-07 08:16:11 |
| 10133 | 2016-03-27 13:59:08 | 1250 | sedan | 1910 | unknown | 111.0 | other | 5000 | 0 | other | audi | no | 2016-03-27 | 0 | 18445 | 2016-04-07 10:45:31 |
| 12919 | 2016-03-07 14:38:00 | 11000 | other | 1955 | manual | 40.0 | unknown | 50000 | 1 | petrol | sonstige_autos | no | 2016-03-07 | 0 | 59556 | 2016-03-14 06:44:36 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 320483 | 2016-03-27 20:45:09 | 17500 | sedan | 1954 | manual | 52.0 | other | 20000 | 7 | petrol | citroen | no | 2016-03-27 | 0 | 55270 | 2016-04-05 18:47:03 |
| 320577 | 2016-03-09 21:56:01 | 5500 | bus | 1956 | manual | 37.0 | unknown | 60000 | 4 | petrol | sonstige_autos | no | 2016-03-09 | 0 | 1900 | 2016-04-06 02:17:54 |
| 320911 | 2016-03-12 00:57:39 | 11500 | sedan | 1800 | unknown | 16.0 | other | 5000 | 6 | petrol | fiat | no | 2016-03-11 | 0 | 16515 | 2016-04-05 19:47:27 |
| 322480 | 2016-03-16 21:56:55 | 6000 | sedan | 1937 | manual | 38.0 | other | 5000 | 0 | petrol | mercedes_benz | no | 2016-03-16 | 0 | 23936 | 2016-03-30 18:47:41 |
| 323141 | 2016-03-07 19:58:44 | 3300 | coupe | 1957 | manual | 40.0 | other | 100000 | 11 | petrol | trabant | no | 2016-03-07 | 0 | 10317 | 2016-03-08 06:45:48 |
259 rows × 16 columns
И откинем соответственно совсем древние автомобили, а вернее ошибочно древние, 300 строк - просто удалим
df = df.drop(df[df['registration_year'] < 1960].index)
df.info()
<class 'pandas.core.frame.DataFrame'> Index: 289715 entries, 1 to 323182 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date_crawled 289715 non-null datetime64[ns] 1 price 289715 non-null int64 2 vehicle_type 289715 non-null object 3 registration_year 289715 non-null int64 4 gearbox 289715 non-null object 5 power 289715 non-null float64 6 model 289715 non-null object 7 kilometer 289715 non-null int64 8 registration_month 289715 non-null int64 9 fuel_type 289715 non-null object 10 brand 289715 non-null object 11 repaired 289715 non-null object 12 date_created 289715 non-null datetime64[ns] 13 number_of_pictures 289715 non-null int64 14 postal_code 289715 non-null int64 15 last_seen 289715 non-null datetime64[ns] dtypes: datetime64[ns](3), float64(1), int64(6), object(6) memory usage: 37.6+ MB
Проделаем то же самое теперь по месяцам
df[(df['registration_year'] == df['date_created'].dt.year) & (df['date_created'].dt.month < df['registration_month'])]
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 22 | 2016-03-23 14:52:51 | 2900 | bus | 2016 | manual | 90.0 | meriva | 150000 | 5 | petrol | opel | no | 2016-03-23 | 0 | 49716 | 2016-03-31 01:16:33 |
| 26 | 2016-03-10 19:38:18 | 5555 | sedan | 2016 | manual | 125.0 | c4 | 125000 | 4 | other | citroen | no | 2016-03-10 | 0 | 31139 | 2016-03-16 09:16:46 |
| 31 | 2016-03-29 16:57:02 | 899 | small | 2016 | manual | 60.0 | clio | 150000 | 6 | petrol | renault | no | 2016-03-29 | 0 | 37075 | 2016-03-29 17:43:07 |
| 51 | 2016-03-07 18:57:08 | 2000 | small | 2016 | manual | 90.0 | punto | 150000 | 11 | petrol | fiat | yes | 2016-03-07 | 0 | 66115 | 2016-03-07 18:57:08 |
| 155 | 2016-03-09 12:53:53 | 14500 | suv | 2016 | manual | 136.0 | sportage | 125000 | 5 | petrol | kia | no | 2016-03-09 | 0 | 49696 | 2016-04-05 23:45:15 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 323017 | 2016-03-29 19:50:03 | 3000 | sedan | 2016 | manual | 82.0 | colt | 150000 | 8 | petrol | mitsubishi | no | 2016-03-29 | 0 | 45472 | 2016-04-06 05:46:43 |
| 323042 | 2016-03-10 23:36:19 | 1350 | coupe | 2016 | manual | 184.0 | clk | 150000 | 8 | other | mercedes_benz | yes | 2016-03-10 | 0 | 26427 | 2016-03-13 02:45:51 |
| 323046 | 2016-03-17 00:56:26 | 2140 | small | 2016 | manual | 80.0 | fiesta | 150000 | 6 | other | ford | no | 2016-03-17 | 0 | 44866 | 2016-03-29 15:45:04 |
| 323119 | 2016-03-05 14:55:29 | 5000 | sedan | 2016 | manual | 120.0 | other | 150000 | 7 | other | citroen | yes | 2016-03-05 | 0 | 15518 | 2016-04-05 11:48:09 |
| 323176 | 2016-03-09 13:37:43 | 5250 | wagon | 2016 | auto | 150.0 | 159 | 150000 | 12 | other | alfa_romeo | no | 2016-03-09 | 0 | 51371 | 2016-03-13 01:44:13 |
10971 rows × 16 columns
df['registration_month'] = df.apply(lambda x: x['date_created'].month
if x['registration_month'] > x['date_created'].month
else x['registration_month'], axis=1)
df[df['registration_month'] > df['date_created'].dt.month]
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen |
|---|
print_pie_bar(df['registration_year'].to_frame())
count 289715.000000 mean 2003.462196 std 7.024391 min 1960.000000 25% 1999.000000 50% 2004.000000 75% 2008.000000 max 2016.000000 Name: registration_year, dtype: float64
Чуть перестроим пайплот:
tmp = df['registration_year'].apply(lambda x: 'old' if x < 2000 else x)
print_pie_bar(tmp.to_frame())
count 289715 unique 18 top old freq 73881 Name: registration_year, dtype: object
На рынке представлено порядка 25% старых автомобилей, что ожидаемо. Объединение можно было бы не делать, но так читаемее
По моделям нет смысла что-то перестраивать, если только взять и построить топ 10 продаваемых моделей
total = df['model'].count()
df['model'].value_counts().head(10).plot(kind='pie', autopct=fmt)
plt.title('Круговая диаграмма признака model')
plt.ylabel('')
plt.show()
fuel_type все нормально, без неожиданностей - в основном бензин или газЯ постарался сохранить максимальное количество данных с минимальными искажениями... Надеюсь получилось)
Распределения признаков нормальным не является, так что сразу построим Фk
df.head()
| date_crawled | price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | date_created | number_of_pictures | postal_code | last_seen | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 2016-03-24 10:58:45 | 18300 | coupe | 2011 | manual | 190.0 | unknown | 125000 | 3 | petrol | audi | yes | 2016-03-24 | 0 | 66954 | 2016-04-07 01:46:50 |
| 2 | 2016-03-14 12:52:21 | 9800 | suv | 2004 | auto | 163.0 | grand | 125000 | 3 | petrol | jeep | no | 2016-03-14 | 0 | 90480 | 2016-04-05 12:47:46 |
| 3 | 2016-03-17 16:54:04 | 1500 | small | 2001 | manual | 75.0 | golf | 150000 | 3 | petrol | volkswagen | no | 2016-03-17 | 0 | 91074 | 2016-03-17 17:40:17 |
| 4 | 2016-03-31 17:25:20 | 3600 | small | 2008 | manual | 69.0 | fabia | 90000 | 3 | petrol | skoda | no | 2016-03-31 | 0 | 60437 | 2016-04-06 10:17:21 |
| 5 | 2016-04-04 17:36:23 | 650 | sedan | 1995 | manual | 102.0 | 3er | 150000 | 4 | petrol | bmw | yes | 2016-04-04 | 0 | 33775 | 2016-04-06 19:17:07 |
В корреляционном анализе также не будем учитывать даты (удалим эти признаки в следующем пункте), почтовый индекс (postal_code), потому что он никак не характеризует авто
interval_columns = df.select_dtypes(include='number').drop(columns=['registration_year', 'registration_month', 'postal_code']).columns
plt.figure(figsize=(10, 8))
sns.heatmap(df.drop(columns=['date_crawled', 'last_seen', 'postal_code', 'date_created']).phik_matrix(interval_cols=interval_columns), annot=True, cmap='Greens')
plt.title(r'корреляция $\phi_K$')
C:\Users\Maxim\anaconda3\Lib\site-packages\phik\data_quality.py:72: UserWarning: Not enough unique value for variable number_of_pictures for analysis 1. Dropping this column warnings.warn(
Text(0.5, 1.0, 'корреляция $\\phi_K$')
Видим мультиколлинеарные признаки - модель и тип кузова - логично, потому что часто у одной модели только один кузов. Сделаем 2 датасета - один без мультиколлинеарности (для линейных моделей), другой с ней для дерева и бустинга. В целом, по матрице корреляций можем сделать вывод о том, чтл сильнее всего влияет год регистрации, модель и мощность двигателя, что соответствует действительности
df_linear = df.drop(columns=['vehicle_type']).copy()
df_linear.info()
<class 'pandas.core.frame.DataFrame'> Index: 289715 entries, 1 to 323182 Data columns (total 15 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date_crawled 289715 non-null datetime64[ns] 1 price 289715 non-null int64 2 registration_year 289715 non-null int64 3 gearbox 289715 non-null object 4 power 289715 non-null float64 5 model 289715 non-null object 6 kilometer 289715 non-null int64 7 registration_month 289715 non-null int64 8 fuel_type 289715 non-null object 9 brand 289715 non-null object 10 repaired 289715 non-null object 11 date_created 289715 non-null datetime64[ns] 12 number_of_pictures 289715 non-null int64 13 postal_code 289715 non-null int64 14 last_seen 289715 non-null datetime64[ns] dtypes: datetime64[ns](3), float64(1), int64(6), object(5) memory usage: 35.4+ MB
df.info()
<class 'pandas.core.frame.DataFrame'> Index: 289715 entries, 1 to 323182 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date_crawled 289715 non-null datetime64[ns] 1 price 289715 non-null int64 2 vehicle_type 289715 non-null object 3 registration_year 289715 non-null int64 4 gearbox 289715 non-null object 5 power 289715 non-null float64 6 model 289715 non-null object 7 kilometer 289715 non-null int64 8 registration_month 289715 non-null int64 9 fuel_type 289715 non-null object 10 brand 289715 non-null object 11 repaired 289715 non-null object 12 date_created 289715 non-null datetime64[ns] 13 number_of_pictures 289715 non-null int64 14 postal_code 289715 non-null int64 15 last_seen 289715 non-null datetime64[ns] dtypes: datetime64[ns](3), float64(1), int64(6), object(6) memory usage: 37.6+ MB
Попробуем посмотрить на скаттерплоты зависимости таргета от признаков
for col_num in df.drop(columns=['registration_year', 'registration_month', 'postal_code', 'price', 'number_of_pictures']).select_dtypes(include='number').columns:
for col_cat in df[['gearbox', 'registration_month', 'fuel_type', 'repaired']].columns:
ax = sns.scatterplot(data=df, x=col_num, y='price', hue=col_cat, alpha=0.3)
plt.title('Зависимость таргета price от ' + 'col_num')
plt.ylabel('цена')
plt.xlabel('значение признака ' + col_num)
plt.show()
C:\Users\Maxim\anaconda3\Lib\site-packages\IPython\core\pylabtools.py:152: UserWarning: Creating legend with loc="best" can be slow with large amounts of data. fig.canvas.print_figure(bytes_io, **kw)
C:\Users\Maxim\anaconda3\Lib\site-packages\IPython\core\pylabtools.py:152: UserWarning: Creating legend with loc="best" can be slow with large amounts of data. fig.canvas.print_figure(bytes_io, **kw)
C:\Users\Maxim\anaconda3\Lib\site-packages\IPython\core\pylabtools.py:152: UserWarning: Creating legend with loc="best" can be slow with large amounts of data. fig.canvas.print_figure(bytes_io, **kw)
Построенные скаттер плоты подтверждают маьрицу корреляций, из интресного можно заметить, что основая масса автомобилей с максимальным пробегом - на газе. Это особо не относится к исследованию моделей, но все же интресно
Вывод по пункту
Начнем с того что подропаем неинофрмативные признаки
Повторим причину дропа каждого признака:
df = df.drop(columns=['date_crawled', 'number_of_pictures', 'postal_code', 'last_seen', 'date_created'])
df.info()
<class 'pandas.core.frame.DataFrame'> Index: 289715 entries, 1 to 323182 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 price 289715 non-null int64 1 vehicle_type 289715 non-null object 2 registration_year 289715 non-null int64 3 gearbox 289715 non-null object 4 power 289715 non-null float64 5 model 289715 non-null object 6 kilometer 289715 non-null int64 7 registration_month 289715 non-null int64 8 fuel_type 289715 non-null object 9 brand 289715 non-null object 10 repaired 289715 non-null object dtypes: float64(1), int64(4), object(6) memory usage: 26.5+ MB
df_linear = df_linear.drop(columns=['date_crawled', 'number_of_pictures', 'postal_code', 'last_seen', 'date_created'])
df_linear.info()
<class 'pandas.core.frame.DataFrame'> Index: 289715 entries, 1 to 323182 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 price 289715 non-null int64 1 registration_year 289715 non-null int64 2 gearbox 289715 non-null object 3 power 289715 non-null float64 4 model 289715 non-null object 5 kilometer 289715 non-null int64 6 registration_month 289715 non-null int64 7 fuel_type 289715 non-null object 8 brand 289715 non-null object 9 repaired 289715 non-null object dtypes: float64(1), int64(4), object(5) memory usage: 24.3+ MB
После проведния масштабного анализа данных, устранения аномалий и удалений признаков, можно еще раз проверить данные на дубликаты.
df.duplicated().sum()
12125
df = df.drop_duplicates().reset_index(drop=True)
df.duplicated().sum()
0
Как и было сказано для задач регрессии, кто бы что ни говорил, а таргету лучше быть нормально распределенным.
UPD: видимо из за выбросов или еще по какой то причине, в данной задаче модель предсказывает точнее без логарифмирования таргета
print_hist_box(df['price'].to_frame())
count 277590.000000 mean 4973.119702 std 4563.949606 min 500.000000 25% 1500.000000 50% 3300.000000 75% 6999.000000 max 20000.000000 Name: price, dtype: float64
Подготовим пайплайны и обучим модели
df.head()
| price | vehicle_type | registration_year | gearbox | power | model | kilometer | registration_month | fuel_type | brand | repaired | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 18300 | coupe | 2011 | manual | 190.0 | unknown | 125000 | 3 | petrol | audi | yes |
| 1 | 9800 | suv | 2004 | auto | 163.0 | grand | 125000 | 3 | petrol | jeep | no |
| 2 | 1500 | small | 2001 | manual | 75.0 | golf | 150000 | 3 | petrol | volkswagen | no |
| 3 | 3600 | small | 2008 | manual | 69.0 | fabia | 90000 | 3 | petrol | skoda | no |
| 4 | 650 | sedan | 1995 | manual | 102.0 | 3er | 150000 | 4 | petrol | bmw | yes |
ohe_cols = ['repaired', 'gearbox', 'fuel_type']
ord_cols_linear = ['model', 'registration_year', 'brand', 'kilometer']
ord_cols = ord_cols_linear + ['vehicle_type']
num_cols = df.select_dtypes(include='number').drop(columns=['registration_year', 'kilometer', 'price']).columns # just power =)
ohe_pl = Pipeline(
[
('ohe_imp', SimpleImputer(strategy='most_frequent', missing_values=np.nan)),
('ohe', OneHotEncoder(drop='first', handle_unknown='ignore'))
]
)
ord_pl = Pipeline( # we use ordinal here because of big variety of each feature
[
('ord_imp', SimpleImputer(strategy='most_frequent', missing_values=np.nan)),
('ord', OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=np.nan)),
('ord_imp_post', SimpleImputer(strategy='most_frequent', missing_values=np.nan))
]
)
col_transformer = ColumnTransformer(
[
('ohe', ohe_pl, ohe_cols),
('ord', ord_pl, ord_cols),
('num', StandardScaler(), num_cols)
],
remainder='passthrough'
)
final_pl = Pipeline(
[
('prep', col_transformer),
('models', LinearRegression())
]
)
X = df.drop(columns=['price'])
y = df['price']
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=RANDOM_STATE)
Попробуем обучить модели линейной регрессии, решащего деререва и 3-х различных реализаций градиентного бустинга:
CatBoostRegressorLGBMRegressorXGBRegressor
Для каждого подберем гиперпараметры с помощью нескольких запусков GridSearchCVНу и соответсвенно для линейной регрессии и оставшихся моделей придется создать разные пайплайны.
params = [
{
'models': [CatBoostRegressor(random_state=RANDOM_STATE, cat_features=ord_cols + ohe_cols, verbose=100)],
'models__learning_rate': [0.1, 0.2],
'models__max_depth': [7, 9],
'models__n_estimators': [500],
'models__subsample': [0.6],
'prep': ['passthrough']
},
{
'models': [XGBRegressor(random_state=RANDOM_STATE), LGBMRegressor(random_state=RANDOM_STATE)],
'models__learning_rate': [0.1, 0.2],
'models__max_depth': [7, 9],
'models__n_estimators': [500],
'models__subsample': [0.6],
'prep__num': ['passthrough']
},
{
'models': [DecisionTreeRegressor(random_state=RANDOM_STATE)],
'models__max_depth': range(3, 5),
'models__min_samples_split': range(3, 5),
'models__min_samples_leaf': range(3, 5),
'prep__num': ['passthrough']
}
]
search = GridSearchCV(
final_pl,
param_grid=params,
n_jobs=-1,
scoring='neg_root_mean_squared_error',
)
search.fit(X_train, y_train)
0: learn: 3913.4638985 total: 282ms remaining: 2m 20s 100: learn: 1670.5269458 total: 13.6s remaining: 53.7s 200: learn: 1592.3126436 total: 26.8s remaining: 39.9s 300: learn: 1540.6186180 total: 41s remaining: 27.1s 400: learn: 1503.7990583 total: 54.6s remaining: 13.5s 499: learn: 1473.2064839 total: 1m 8s remaining: 0us
GridSearchCV(estimator=Pipeline(steps=[('prep',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('ohe_imp',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore'))]),
['repaired',
'gearbox',
'fuel_type']),
('ord',
Pipeline(steps=[('ord_imp',
SimpleImputer(strategy='most_frequent'))...
'models__learning_rate': [0.1, 0.2],
'models__max_depth': [7, 9],
'models__n_estimators': [500],
'models__subsample': [0.6],
'prep__num': ['passthrough']},
{'models': [DecisionTreeRegressor(random_state=42)],
'models__max_depth': range(3, 5),
'models__min_samples_leaf': range(3, 5),
'models__min_samples_split': range(3, 5),
'prep__num': ['passthrough']}],
scoring='neg_root_mean_squared_error')In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. GridSearchCV(estimator=Pipeline(steps=[('prep',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('ohe_imp',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore'))]),
['repaired',
'gearbox',
'fuel_type']),
('ord',
Pipeline(steps=[('ord_imp',
SimpleImputer(strategy='most_frequent'))...
'models__learning_rate': [0.1, 0.2],
'models__max_depth': [7, 9],
'models__n_estimators': [500],
'models__subsample': [0.6],
'prep__num': ['passthrough']},
{'models': [DecisionTreeRegressor(random_state=42)],
'models__max_depth': range(3, 5),
'models__min_samples_leaf': range(3, 5),
'models__min_samples_split': range(3, 5),
'prep__num': ['passthrough']}],
scoring='neg_root_mean_squared_error')Pipeline(steps=[('prep',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('ohe_imp',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore'))]),
['repaired', 'gearbox',
'fuel_type']),
('ord',
Pipeline(steps=[('ord_imp',
SimpleImputer(strategy='most_frequent')),
('ord',
OrdinalEncoder(handle_unknown='use_encoded_value',
unknown_value=nan)),
('ord_imp_post',
SimpleImputer(strategy='most_frequent'))]),
['model', 'registration_year',
'brand', 'kilometer',
'vehicle_type']),
('num', StandardScaler(),
Index(['power', 'registration_month'], dtype='object'))])),
('models', LinearRegression())])ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('ohe_imp',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore'))]),
['repaired', 'gearbox', 'fuel_type']),
('ord',
Pipeline(steps=[('ord_imp',
SimpleImputer(strategy='most_frequent')),
('ord',
OrdinalEncoder(handle_unknown='use_encoded_value',
unknown_value=nan)),
('ord_imp_post',
SimpleImputer(strategy='most_frequent'))]),
['model', 'registration_year', 'brand',
'kilometer', 'vehicle_type']),
('num', StandardScaler(),
Index(['power', 'registration_month'], dtype='object'))])['repaired', 'gearbox', 'fuel_type']
SimpleImputer(strategy='most_frequent')
OneHotEncoder(drop='first', handle_unknown='ignore')
['model', 'registration_year', 'brand', 'kilometer', 'vehicle_type']
SimpleImputer(strategy='most_frequent')
OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=nan)
SimpleImputer(strategy='most_frequent')
Index(['power', 'registration_month'], dtype='object')
StandardScaler()
passthrough
LinearRegression()
search.best_estimator_
Pipeline(steps=[('prep', 'passthrough'),
('models',
<catboost.core.CatBoostRegressor object at 0x000001CD2F1C9590>)])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. Pipeline(steps=[('prep', 'passthrough'),
('models',
<catboost.core.CatBoostRegressor object at 0x000001CD2F1C9590>)])passthrough
<catboost.core.CatBoostRegressor object at 0x000001CD2F1C9590>
np.abs(search.best_score_)
1686.4042540556645
search.best_params_
{'models': <catboost.core.CatBoostRegressor at 0x1cd35a01dd0>,
'models__learning_rate': 0.2,
'models__max_depth': 9,
'models__n_estimators': 500,
'models__subsample': 0.6,
'prep': 'passthrough'}
Теперь посмотрим на то, как с задачей справятся линейные модели
X_linear = df_linear.drop(columns=['price'])
y_linear = df_linear['price']
X_train_linear, X_test_linear, y_train_linear, y_test_linear = train_test_split(X_linear, y_linear, random_state=RANDOM_STATE)
col_transformer = ColumnTransformer(
[
('ohe', ohe_pl, ohe_cols),
('ord', ord_pl, ord_cols_linear),
('num', StandardScaler(), num_cols)
],
remainder='passthrough'
)
final_pl_linear = Pipeline(
[
('prep', col_transformer),
('models', LinearRegression())
]
)
params_linear = [
{
'models': [Ridge(random_state=RANDOM_STATE), Lasso(random_state=RANDOM_STATE)],
'models__alpha': [0.1, 0.5, 1, 2],
},
{
'models': [LinearRegression()]
}
]
search_linear = GridSearchCV(
final_pl_linear,
param_grid=params_linear,
n_jobs=-1,
scoring='neg_root_mean_squared_error',
)
search_linear.fit(X_train_linear, y_train_linear)
GridSearchCV(estimator=Pipeline(steps=[('prep',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('ohe_imp',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore'))]),
['repaired',
'gearbox',
'fuel_type']),
('ord',
Pipeline(steps=[('ord_imp',
SimpleImputer(strategy='most_frequent'))...
SimpleImputer(strategy='most_frequent'))]),
['model',
'registration_year',
'brand',
'kilometer']),
('num',
StandardScaler(),
Index(['power', 'registration_month'], dtype='object'))])),
('models', LinearRegression())]),
n_jobs=-1,
param_grid=[{'models': [Ridge(random_state=42),
Lasso(random_state=42)],
'models__alpha': [0.1, 0.5, 1, 2]},
{'models': [LinearRegression()]}],
scoring='neg_root_mean_squared_error')In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. GridSearchCV(estimator=Pipeline(steps=[('prep',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('ohe_imp',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore'))]),
['repaired',
'gearbox',
'fuel_type']),
('ord',
Pipeline(steps=[('ord_imp',
SimpleImputer(strategy='most_frequent'))...
SimpleImputer(strategy='most_frequent'))]),
['model',
'registration_year',
'brand',
'kilometer']),
('num',
StandardScaler(),
Index(['power', 'registration_month'], dtype='object'))])),
('models', LinearRegression())]),
n_jobs=-1,
param_grid=[{'models': [Ridge(random_state=42),
Lasso(random_state=42)],
'models__alpha': [0.1, 0.5, 1, 2]},
{'models': [LinearRegression()]}],
scoring='neg_root_mean_squared_error')Pipeline(steps=[('prep',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('ohe_imp',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore'))]),
['repaired', 'gearbox',
'fuel_type']),
('ord',
Pipeline(steps=[('ord_imp',
SimpleImputer(strategy='most_frequent')),
('ord',
OrdinalEncoder(handle_unknown='use_encoded_value',
unknown_value=nan)),
('ord_imp_post',
SimpleImputer(strategy='most_frequent'))]),
['model', 'registration_year',
'brand', 'kilometer']),
('num', StandardScaler(),
Index(['power', 'registration_month'], dtype='object'))])),
('models', LinearRegression())])ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('ohe_imp',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore'))]),
['repaired', 'gearbox', 'fuel_type']),
('ord',
Pipeline(steps=[('ord_imp',
SimpleImputer(strategy='most_frequent')),
('ord',
OrdinalEncoder(handle_unknown='use_encoded_value',
unknown_value=nan)),
('ord_imp_post',
SimpleImputer(strategy='most_frequent'))]),
['model', 'registration_year', 'brand',
'kilometer']),
('num', StandardScaler(),
Index(['power', 'registration_month'], dtype='object'))])['repaired', 'gearbox', 'fuel_type']
SimpleImputer(strategy='most_frequent')
OneHotEncoder(drop='first', handle_unknown='ignore')
['model', 'registration_year', 'brand', 'kilometer']
SimpleImputer(strategy='most_frequent')
OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=nan)
SimpleImputer(strategy='most_frequent')
Index(['power', 'registration_month'], dtype='object')
StandardScaler()
passthrough
LinearRegression()
np.abs(search_linear.best_score_)
3195.2506271255006
Как видим, бустинг справляется с явным отрывом
На кросс валидации получаем RMSE = 1686.4. На самом деле это немало с учетом того, что у нас цены на автомобиль достигают максимум 20000. Думаю, что проблема как раз в данных. Что много странных объявлений, и даже несмотря на то, что большую часть почистили, все равно остались странные объявления, а также не хвататает других характреристик
Вывод по пукту
CatBoostRegressorLGBMRegressorXGBRegressorGridSearchCVCatBoostRegressor со значением метрики 1686.4 при кросс валидации.Итак, в прошлом пункте мы получили лучшую модель по метрике - XGBRegressor. Посмотрим теперь на другие необзодимые параметры моделей, необходимые заказчику - время обучения и время предсказания. Будем сравнивать лучшие модели по качеству:
pd.DataFrame(search.cv_results_).\
sort_values(by='mean_test_score', ascending=False)\
[['mean_fit_time', 'std_fit_time', 'mean_score_time', 'std_score_time', 'param_models', 'mean_test_score', 'std_test_score']]
| mean_fit_time | std_fit_time | mean_score_time | std_score_time | param_models | mean_test_score | std_test_score | |
|---|---|---|---|---|---|---|---|
| 3 | 334.111707 | 113.307177 | 0.480914 | 0.074434 | <catboost.core.CatBoostRegressor object at 0x0... | -1686.404254 | 2.728665 |
| 11 | 30.591402 | 2.367052 | 0.709930 | 0.144207 | LGBMRegressor(random_state=42) | -1686.794844 | 6.651305 |
| 10 | 71.554471 | 42.740585 | 0.500861 | 0.053883 | LGBMRegressor(random_state=42) | -1686.962699 | 5.637834 |
| 4 | 35.492498 | 2.029703 | 2.933756 | 0.706011 | XGBRegressor(base_score=None, booster=None, ca... | -1687.320708 | 7.052145 |
| 5 | 46.960633 | 4.304858 | 3.962803 | 0.167691 | XGBRegressor(base_score=None, booster=None, ca... | -1689.127766 | 8.625721 |
| 1 | 549.127991 | 8.070734 | 0.689756 | 0.119755 | <catboost.core.CatBoostRegressor object at 0x0... | -1697.310033 | 4.573720 |
| 9 | 131.346394 | 10.341569 | 0.669609 | 0.125979 | LGBMRegressor(random_state=42) | -1700.629511 | 4.652339 |
| 8 | 147.470280 | 9.595887 | 0.809036 | 0.187975 | LGBMRegressor(random_state=42) | -1705.943031 | 5.140867 |
| 2 | 440.276518 | 18.201139 | 1.081708 | 0.317549 | <catboost.core.CatBoostRegressor object at 0x0... | -1706.199186 | 2.034028 |
| 6 | 35.553734 | 3.304349 | 2.611018 | 0.294958 | XGBRegressor(base_score=None, booster=None, ca... | -1710.642887 | 8.940031 |
| 0 | 431.352636 | 16.845710 | 1.240284 | 0.518650 | <catboost.core.CatBoostRegressor object at 0x0... | -1726.355039 | 2.196729 |
| 7 | 42.914252 | 1.925264 | 3.221985 | 0.207335 | XGBRegressor(base_score=None, booster=None, ca... | -1739.051131 | 9.727837 |
| 16 | 2.384025 | 0.024189 | 0.316953 | 0.051520 | DecisionTreeRegressor(random_state=42) | -2775.708480 | 6.411538 |
| 17 | 2.389810 | 0.052476 | 0.292418 | 0.017405 | DecisionTreeRegressor(random_state=42) | -2775.708480 | 6.411538 |
| 18 | 2.292271 | 0.016432 | 0.296607 | 0.024886 | DecisionTreeRegressor(random_state=42) | -2775.708480 | 6.411538 |
| 19 | 2.094799 | 0.277725 | 0.212232 | 0.038557 | DecisionTreeRegressor(random_state=42) | -2775.708480 | 6.411538 |
| 12 | 1.691477 | 0.176606 | 0.259107 | 0.072424 | DecisionTreeRegressor(random_state=42) | -3078.449037 | 8.848991 |
| 13 | 2.349318 | 0.103077 | 0.281049 | 0.050860 | DecisionTreeRegressor(random_state=42) | -3078.449037 | 8.848991 |
| 14 | 1.881569 | 0.030030 | 0.265091 | 0.017789 | DecisionTreeRegressor(random_state=42) | -3078.449037 | 8.848991 |
| 15 | 2.075251 | 0.156666 | 0.285237 | 0.013675 | DecisionTreeRegressor(random_state=42) | -3078.449037 | 8.848991 |
Видим, что наша лучшая модель по скору медленнее всего обучается и быстрее всего предсказывает (среди бустингов). Стоит отметить и то, что все 3 имплементации бустига могут подойти, и если заказчику важно время обучения, то лучше взять, конечно, XGBoost. Но остановимся мы именно на CatBoost, потому что эта модель имеет ряд преимуществ, например, то, что просто из коробки она будет наиболее точной. Перепроверим данные о скорости обучения и предсказания у нашей луйшей модели
%%time
search.best_estimator_.fit(X_train, y_train)
0: learn: 3913.4638985 total: 109ms remaining: 54.2s 100: learn: 1670.5269458 total: 12.6s remaining: 49.7s 200: learn: 1592.3126436 total: 25.5s remaining: 37.9s 300: learn: 1540.6186180 total: 39.1s remaining: 25.8s 400: learn: 1503.7990583 total: 52.9s remaining: 13.1s 499: learn: 1473.2064839 total: 1m 5s remaining: 0us CPU times: total: 42.6 s Wall time: 1min 6s
Pipeline(steps=[('prep', 'passthrough'),
('models',
<catboost.core.CatBoostRegressor object at 0x000001CD2F1C9590>)])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. Pipeline(steps=[('prep', 'passthrough'),
('models',
<catboost.core.CatBoostRegressor object at 0x000001CD2F1C9590>)])passthrough
<catboost.core.CatBoostRegressor object at 0x000001CD2F1C9590>
%%time
search.best_estimator_.predict(X_test)
CPU times: total: 1.48 s Wall time: 280 ms
array([ 6990.55414429, 3425.57212621, 8608.55199744, ...,
10356.11346996, 1450.10221584, 3993.71523731])
Обучение занимает ~ 37 секунд процессорного времени и ~ 1.1 минуты "настенного времени", то есть часы, находящиеся за пределами ПК посчитают 1.1 минуты.
Предсказание занимает ~ 1.28 процессорного времени и ~ 250 мс "настенного времени"
ну и выполним еще раз предсказание на тестовой выборке и посчитаем RMSE:
root_mean_squared_error(y_test, search.predict(X_test))
1689.9939650633469
Видим небольшое переобучение, но на кросс-валидации это допустимо
На всякий случай проверим нашу модель на адекватность и сделаем прогноз Дамми регрессором.
dummy_pl = Pipeline(
[
('prep', col_transformer),
('models', DummyRegressor())
]
)
dummy_pl.fit(X_train, y_train)
root_mean_squared_error(y_test, dummy_pl.predict(X_test))
4579.7435214959105
Наша модель намного лучше
В данном проекте были проанализированы данные об анкетах о продаже авто.
CatBoostRegressorLGBMRegressorXGBRegressorGridSearchCVXGBRegressor со значением метрики 1690.0 на тестовой выборке.
Проанализировав и другие модели, мы поняли, что оптимальным решением для заказчика станет как раз наша лучшая модель с результатми:XGBoost.CatBoost.